Skip to content

Rest / Spread

In this lesson, we're going to cover two of my favourite bits of modern JS syntax: rest and spread.

Philosophically, rest and spread do opposite things. “Rest” will gather up a bunch of individual values, while “spread” will unpack a set of gathered values.

Both operations use the same syntax, a triple dot (...). Bewilderingly, it's technically not an operator, though it really feels like it should be! To keep consistent with the formal literature, I'll refer to it as syntax rather than an operator.

Rest Parameters

Let's suppose we're writing a function that takes a variable number of parameters.

For example, maybe we want to create a function that will add all of the numbers provided, no matter how many there are:

addNums(1, 1); // 2
addNums(1, 2, 3, 4); // 10
addNums(1, 10, 100, 1000, 10000); // 11111

We can do this by using rest parameters:

function addNums(...nums) {
let sum = 0;
nums.forEach(num => {
sum += num;
});
return sum;
}

nums is a single parameter that will gather all other parameters into an array:

function logArgs(...args) {
console.log(args);
}
logArgs(1, 2, 'hi!');
// logs: [1, 2, 'hi']

Like any other function parameter, we can name it whatever we want. It's common to use ...rest, but it could just as easily be ...allTheThings.

Sometimes, we'll have one or more known/fixed parameters, and several variable ones. We can use a rest parameter in tandem with other parameters, like this:

function removeFirstArg(first, ...rest) {
return rest;
}
removeFirstArg(1, 2, 3, 4); // Produces [2, 3, 4]

That said, there are some rules. You're only allowed to have a single rest parameter, and it has to come last.

Neither of these functions are valid:

function nope(...firstRest, ...rest) {}
function notAllowed(...rest, second, third) {}

Spread syntax

Spread syntax is the opposite of rest parameters. Instead of collecting a bunch of parameters into an array, it spreads an array of data into individual arguments.

For example:

function createDate(year, month, day) {
return new Date(year, month, day);
}
const myDateInfo = [2020, 01, 01];
createDate(...myDateInfo);

We “unpack” myDateInfo so that it provides 3 individual arguments, rather than a single array argument. It's equivalent to doing this:

const myDateInfo = [2020, 01, 01];
createDate(
myDateInfo[0],
myDateInfo[1],
myDateInfo[2]
);

There are a bunch of handy usecases for this.

For example, we can use it to copy the children from one array to another:

const myArray = [1, 2, 3, 4];
const arrayCopy = [...myArray];
console.log(arrayCopy); // [1, 2, 3, 4];
console.log(myArray === arrayCopy); // false. Different arrays.

The ...myArray syntax will expand out into individual values. Because we've wrapped it in square brackets ([ ]), it will create a brand-new array that holds these expanded values.

Similarly, we can use it to merge two arrays:

const myNumbers = [1, 2, 3];
const yourNumbers = [4, 5, 6];
const ourNumbers = [...myNumbers, ...yourNumbers];
console.log(ourNumbers); // [1, 2, 3, 4, 5, 6]

Usage with objects

The earliest version of this syntax only worked with arrays, but nowadays we can use it with objects as well!

For example, we can create a new copy of an existing object:

const originalObject = {
latitude: 1.234,
longitude: 4.321,
};
const clonedObj = { ...originalObject };

We can extend existing objects into new objects with additional properties:

const sharedCharacteristics = {
species: 'human',
location: 'earth',
};
const human1 = {
...sharedCharacteristics,
name: 'Tina',
eyeColor: 'green',
};
const human2 = {
...sharedCharacteristics,
name: 'James',
eyeColor: 'brown',
};

And we can merge two or more objects like this:

const myObj = { hi: 5 };
const yourObj = { bye: 10 };
const ourObj = { ...myObj, ...yourObj };
console.log(ourObj); // { hi: 5, bye: 10 }

Conflicts

When we spread arrays, we don't have to worry about conflicts. We'll always get a new array which includes every item from every spread array:

const arr1 = [1, 2, 3];
const arr2 = [3, 4, 5];
const merged = [...arr1, ...arr2];
console.log(merged);
// [1, 2, 3, 3, 4, 5] <- 6 items

Objects are a bit different, though. What happens when multiple objects define the same key?

const obj1 = {
color: 'red',
border: '2px solid',
zIndex: 5,
};
const obj2 = {
fontSize: '2rem',
fontWeight: 500,
zIndex: 10,
};
const merged = { ...obj1, ...obj2 };
console.log(merged);
/*
{
color: 'red',
border: '2px solid',
fontSize: '2rem',
fontWeight: 500,
zIndex: ???,
}
*/

Will zIndex be set to 5 or 10? How does it resolve conflicts?

It resolves them by prioritizing later items over earlier ones. And so in this case, zIndex will be set to 10.

Here's how I think about it: the spread syntax works sequentially. We spread the first object, and then the second object.

Knowing this logic, it allows us to create "defaults" that can be overwritten:

const defaultValues = {
type: 'user',
password: '12345',
};
const firstUser = {
...defaultValues,
username: 'helloWorld',
};
const secondUser = {
...defaultValues,
username: 'cheeseCrackers',
password: 'salty-5%-heroic-cheesy-adventure'
};
console.log(firstUser);
/*
{
type: 'user',
password: '12345',
username: 'helloWorld',
}
*/
console.log(secondUser);
/*
{
type: 'user',
username: 'cheeseCrackers',
password: 'salty-5%-heroic-cheesy-adventure',
}
*/

firstUser doesn't define a password, and so it'll use the default password, 12345. secondUser does provide a password, and so it will overwrite the default value.

It's important that we spread the default values first. Otherwise, the logic will be flipped, and all users will use the default password!

const whoops = {
username: 'misterSkydive',
password: '10,000-free-falling-watermelons :O'
...defaultValues,
};
console.log(whoops);
/*
{
username: 'misterSkydive',
type: 'user',
password: '12345',
}
*/

Rest/spread syntax is incredibly powerful. We'll see plenty of examples of how to use it effectively throughout this course.